package res.input;

import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;

import res.Pair;
import res.exceptions.InvalidInputGraphException;
import res.graph.DirectedSimpleGraph;
import res.graph.Edge;
import res.graph.EdgeConstraints;
import res.graph.Graph;
import res.graph.ReadableGraph;
import res.input.enums.RoadSystem;

public class DefaultXMLGraphReader implements GraphReader {
	protected RoadSystem roadsystem = RoadSystem.NONE;
	protected String optseparator = ":";
	protected ReadableGraph g = null;
	protected Hashtable< Pair<Integer, Integer>, Edge > HSedges;
	protected HashSet<EdgeConstraints> HSedgesConstraints;
	protected final Element root;
	protected int numvertices = 0;

	public DefaultXMLGraphReader(File filein) throws IOException,
			InvalidInputGraphException {
		try {
			Document doc;
			String separator = null;

			if (!filein.exists())
				Logger.getLogger(this.getClass().getName()).severe(
						"O arquivo do grafo não foi encontrado.");

			doc = new SAXBuilder().build(filein);
			root = doc.getRootElement();

			if (root.getAttribute("separator") != null)
				separator = root.getAttributeValue("separator");
			if (separator != null && !separator.equals(""))
				optseparator = Pattern.quote(separator);
			else
				Logger.getLogger(GridGraphReader.class.getName())
						.info("A expressão de separação de valores de opções não foi informada. Assumindo o valor padrão ["
								+ optseparator + ".");
			
			HSedges = new Hashtable<Pair<Integer,Integer>, Edge>();
			HSedgesConstraints = new HashSet<EdgeConstraints>();
		} catch (JDOMException e) {
			throw new InvalidInputGraphException(e.getMessage());
		}
	}

	public DefaultXMLGraphReader(String filepath) throws IOException,
			InvalidInputGraphException {
		this(new File(filepath));
	}

	@Override
	public Graph readGraph() throws InvalidInputGraphException {
		if(g == null) {
			readParams(root.getChildren("param"));
			readOptions(root.getChildren("option"));
			
			g = new DirectedSimpleGraph(numvertices);
			
			readEdges(root.getChildren("edges"));
			calculateSuccessors();
			calculateNeighbours();
	
			Logger.getLogger(GridGraphReader.class.getName()).info(
					"O grafo e suas opções foram lidos.");
			Logger.getLogger(GridGraphReader.class.getName())
					.info(String
							.format("VERTICES %d. ARESTAS %d.",
									numvertices, HSedges.size()));
		}

		return g;
	}
	
	protected void calculateInitialSets(Edge edge) {}

	protected void calculateSuccessors() {
		for(EdgeConstraints constr : HSedgesConstraints) {
			Edge edge = HSedges.get(constr.getEdge());
			Edge etmp = null;
			
			for(Pair<Integer, Integer> f : constr.getForbidConstraints()) {
				edge.removeSuccessor(HSedges.get(f));
			}

			for(Pair<Integer, Integer> f : constr.getAllowConstraints()) {
				edge.addSuccessor(HSedges.get(f));
			}
		}
	}

	protected void calculateNeighbours() {
		for(EdgeConstraints constr : HSedgesConstraints) {
			Edge edge = HSedges.get(constr.getEdge());
			Edge etmp = null;
			
			calculateInitialSets(edge);
			
			for(Pair<Integer, Integer> f : constr.getCompetitors()) {
				etmp = HSedges.get(f);
				
				if(etmp.getSource().getOrd() != edge.getSource().getOrd() || etmp.getTarget().getOrd() != edge.getTarget().getOrd()) {
					edge.addCompetitor(etmp);
					etmp.addCompetitor(edge);
					edge.removePartner(etmp);
					etmp.removePartner(edge);
				}
			}
	
			for(Pair<Integer, Integer> f : constr.getPartners()) {
				etmp = HSedges.get(f);
				
				if(etmp.getSource().getOrd() != edge.getSource().getOrd() || etmp.getTarget().getOrd() != edge.getTarget().getOrd()) {
					edge.removeCompetitor(etmp);
					etmp.removeCompetitor(edge);
					edge.addPartner(etmp);
					etmp.addPartner(edge);
				}
			}
		}
	}

	protected boolean isValidEdge(int from, int to) {
		return (from < numvertices && to < numvertices)
			&& (from >= 0 && to >= 0);
	}

	protected void readEdges(List<Element> edgessections)
			throws InvalidInputGraphException {
		if (edgessections.size() >= 1) {
			long i = 0;

			for (Element e : edgessections) {
				List<Element> edges = e.getChildren("edge");

				for (Element edge : edges) {
					int from;
					int to;
					i++;
					
					try {
						from = Integer.parseInt(edge.getAttributeValue("from", "-1"));
						to = Integer.parseInt(edge.getAttributeValue("to", "-1"));
					} catch (NumberFormatException ex) {
						Logger.getLogger(GridGraphReader.class.getName())
								.severe("Os valores informados para os vértices da "+i+"a aresta não são válidos. Esta aresta foi ignorada.");
						continue;
					}

					if (!isValidEdge(from, to)) {
						Logger.getLogger(GridGraphReader.class.getName())
								.severe("Os valores de vértices da "
										+ i
										+ "a aresta foram informados erroneamente ou mesmo não foram informados. Valores não-negativos são esperados.");
					} else {
						double weight = 0.0d;

						try { 
							if (edge.getAttributeValue("weight") != null)
								weight = Double.parseDouble(edge.getAttributeValue("weight"));
							else
								Logger.getLogger(
										GridGraphReader.class.getName())
										.info("O peso da "+i+"a aresta não foi informado. Assumindo o valor 0 por padrão.");
						} catch (NumberFormatException ex) {
							Logger.getLogger(
									GridGraphReader.class.getName())
									.severe("O valor informado para o peso da "+i+"a aresta não é válido. Assumindo o valor 0 por padrão.");
						}
						
						Edge newEdge = g.addEdge(from, to, weight);
						Pair<Integer, Integer> pedge = new Pair<Integer, Integer>(from, to);
						EdgeConstraints newConstraints = new EdgeConstraints(pedge);
						String[] paramvalues;
						
						try {
							if (edge.getAttributeValue("allow") != null) {
								paramvalues = edge.getAttributeValue(
										"allow").split(optseparator);
								for (String s : paramvalues)
									newConstraints.addAllowed(g.getVertex(Integer.parseInt(s)).getOrd());
							}
							if (edge.getAttributeValue("forbid") != null) {
								paramvalues = edge.getAttributeValue(
										"forbid").split(optseparator);
								for (String s : paramvalues)
									newConstraints.addForbidden(g.getVertex(Integer.parseInt(s)).getOrd());
							}
							if (edge.getAttributeValue("partners") != null) {
								paramvalues = edge.getAttributeValue(
										"partners").split(optseparator);
								for (String s : paramvalues)
									newConstraints.addPartner(g.getVertex(Integer.parseInt(s)).getOrd());
							}
							if (edge.getAttributeValue("competitors") != null) {
								paramvalues = edge.getAttributeValue(
										"competitors").split(optseparator);
								for (String s : paramvalues)
									newConstraints.addCompetitor(g.getVertex(Integer.parseInt(s)).getOrd());
							}
						} catch (NumberFormatException ex) {
							Logger.getLogger(
									GridGraphReader.class.getName())
									.severe("Esperava-se o número de um vértice dentre o parâmetros opcionais da "
											+ i + "a aresta.\n\tAs opções encontradas para esta aresta após este erro foram ignoradas.");
						} catch (ArrayIndexOutOfBoundsException ex) {
							Logger.getLogger(
									GridGraphReader.class.getName())
									.severe("Esperava-se o número de um vértice dentre o parâmetros opcionais da "
											+ i + "a aresta.\n\tAs opções encontradas para esta aresta após este erro foram ignoradas.");
						} finally {
							HSedges.put(pedge, newEdge);
							HSedgesConstraints.add(newConstraints);
						}
					}
				}
			}

			if (edgessections.size() > 1)
				Logger.getLogger(GridGraphReader.class.getName())
						.warning(
								"Mais de uma seção de arestas foi encontrada. Todas foram consideradas.");
		} else
			Logger.getLogger(GridGraphReader.class.getName())
					.warning(
							"Nenhuma seção de arestas não foi encontrada. Assumindo 0 arestas no grafo.");

		if (HSedges.isEmpty())
			Logger.getLogger(GridGraphReader.class.getName()).info(
					"Nenhuma aresta foi informada.");
	}

	protected void readParams(List<Element> params)
			throws InvalidInputGraphException {
		
		if (params.size() >= 1) {
			String paramname;
			String paramvalue;

			for (Element e : params) {
				paramname = e.getAttributeValue("name");
				paramvalue = e.getAttributeValue("value");

				if (paramname != null && paramvalue != null) {
					try {
						if (paramname.equals("vertices"))
							numvertices = Integer.parseInt(paramvalue);
					} catch (NumberFormatException ex) {
						Logger.getLogger(GridGraphReader.class.getName())
								.severe("Esperava-se um inteiro como valor do parâmetro ["
										+ paramname + "].");
						throw new InvalidInputGraphException();
					}
				} else
					Logger.getLogger(GridGraphReader.class.getName())
							.severe("O nome ou o valor de algum parâmetro não foi informado. Ambos são mandatórios para opções.");
			}

			if (params.size() > 1)
				Logger.getLogger(GridGraphReader.class.getName())
						.warning(
								"Um número maior de parâmetros do que o necessário foi informado. Apenas os últimos valores válidos foram reconhecidos.");
		} else {
			Logger.getLogger(GridGraphReader.class.getName()).severe(
					"Nem todos os parâmetros foram informados.");
			throw new InvalidInputGraphException();
		}

		if (numvertices <= 0) {
			Logger.getLogger(GridGraphReader.class.getName())
					.severe("O número de vértices do grafo deve ser estritamente positivo.");
			throw new InvalidInputGraphException();
		}
	}

	protected void readOptions(List<Element> options) throws InvalidInputGraphException {
		if (options.isEmpty())
			Logger.getLogger(GridGraphReader.class.getName()).info(
					"Nenhuma opção foi informada.");
		else {
			String optname;
			String[] optvalues;
			int i = 0;

			for (Element e : options) {
				optname = e.getAttributeValue("name");
				optvalues = e.getAttributeValue("values", "").split(optseparator);

				i++;
				
				if (optname != null) {
					if (optname.equalsIgnoreCase("roadsystem")) {
						if (optvalues.length >= 1) {
							if (RoadSystem.LEFT.stringEquals(optvalues[0]))
								roadsystem = RoadSystem.LEFT;
							else if (RoadSystem.RIGHT.stringEquals(optvalues[0]))
								roadsystem = RoadSystem.RIGHT;
							else {
								roadsystem = RoadSystem.NONE;
								Logger.getLogger(
										GridGraphReader.class.getName()).info(
										"Um valor não reconhecido foi informado para a opção ["
												+ optname
												+ "]. Atribuindo ao padrão <"
												+ RoadSystem.NONE + ">.");
							}
						}
					} else {
						Logger.getLogger(GridGraphReader.class.getName())
								.warning(
										"A opção ["
												+ optname
												+ "] não foi reconhecida ou não é suportada atualmente.");
					}
				} else
					Logger.getLogger(GridGraphReader.class.getName())
							.severe("O nome da "+i+"a opção não foi informado. Ambos são mandatórios para opções.");
			}
		}
	}

}
